<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Controller
 * @subpackage Router
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @version    $Id: Rewrite.php 18951 2009-11-12 16:26:19Z alexander $
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

/** Zend_Controller_Router_Abstract */
require_once 'Zend/Controller/Router/Abstract.php';

/** Zend_Controller_Router_Route */
require_once 'Zend/Controller/Router/Route.php';

/**
 * Ruby routing based Router.
 *
 * @package    Zend_Controller
 * @subpackage Router
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @see        http://manuals.rubyonrails.com/read/chapter/65
 */
class Zend_Controller_Router_Rewrite extends Zend_Controller_Router_Abstract
{

	/**
	 * Whether or not to use default routes
	 *
	 * @var boolean
	 */
	protected $_useDefaultRoutes = true;

	/**
	 * Array of routes to match against
	 *
	 * @var array
	 */
	protected $_routes = array();

	/**
	 * Currently matched route
	 *
	 * @var Zend_Controller_Router_Route_Interface
	 */
	protected $_currentRoute = null;

	/**
	 * Global parameters given to all routes
	 *
	 * @var array
	 */
	protected $_globalParams = array();

	/**
	 * Separator to use with chain names
	 *
	 * @var string
	 */
	protected $_chainNameSeparator = '-';

	/**
	 * Add default routes which are used to mimic basic router behaviour
	 *
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function addDefaultRoutes()
	{
		if (!$this->hasRoute('default')) {
			$dispatcher = $this->getFrontController()->getDispatcher();
			$request = $this->getFrontController()->getRequest();

			require_once 'Zend/Controller/Router/Route/Module.php';
			$compat = new Zend_Controller_Router_Route_Module(array(), $dispatcher, $request);

			$this->_routes = array_merge(array('default' => $compat), $this->_routes);
		}

		return $this;
	}

	/**
	 * Add route to the route chain
	 *
	 * If route contains method setRequest(), it is initialized with a request object
	 *
	 * @param  string                                 $name       Name of the route
	 * @param  Zend_Controller_Router_Route_Interface $route      Instance of the route
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function addRoute($name, Zend_Controller_Router_Route_Interface $route)
	{
		if (method_exists($route, 'setRequest')) {
			$route->setRequest($this->getFrontController()->getRequest());
		}

		$this->_routes[$name] = $route;

		return $this;
	}

	/**
	 * Add routes to the route chain
	 *
	 * @param  array $routes Array of routes with names as keys and routes as values
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function addRoutes($routes) {
		foreach ($routes as $name => $route) {
			$this->addRoute($name, $route);
		}

		return $this;
	}

	/**
	 * Create routes out of Zend_Config configuration
	 *
	 * Example INI:
	 * routes.archive.route = "archive/:year/*"
	 * routes.archive.defaults.controller = archive
	 * routes.archive.defaults.action = show
	 * routes.archive.defaults.year = 2000
	 * routes.archive.reqs.year = "\d+"
	 *
	 * routes.news.type = "Zend_Controller_Router_Route_Static"
	 * routes.news.route = "news"
	 * routes.news.defaults.controller = "news"
	 * routes.news.defaults.action = "list"
	 *
	 * And finally after you have created a Zend_Config with above ini:
	 * $router = new Zend_Controller_Router_Rewrite();
	 * $router->addConfig($config, 'routes');
	 *
	 * @param  Zend_Config $config  Configuration object
	 * @param  string      $section Name of the config section containing route's definitions
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function addConfig(Zend_Config $config, $section = null)
	{
		if ($section !== null) {
			if ($config->{$section} === null) {
				require_once 'Zend/Controller/Router/Exception.php';
				throw new Zend_Controller_Router_Exception("No route configuration in section '{$section}'");
			}

			$config = $config->{$section};
		}

		foreach ($config as $name => $info) {
			$route = $this->_getRouteFromConfig($info);

			if ($route instanceof Zend_Controller_Router_Route_Chain) {
				if (!isset($info->chain)) {
					require_once 'Zend/Controller/Router/Exception.php';
					throw new Zend_Controller_Router_Exception("No chain defined");
				}

				if ($info->chain instanceof Zend_Config) {
					$childRouteNames = $info->chain;
				} else {
					$childRouteNames = explode(',', $info->chain);
				}

				foreach ($childRouteNames as $childRouteName) {
					$childRoute = $this->getRoute(trim($childRouteName));
					$route->chain($childRoute);
				}

				$this->addRoute($name, $route);
			} elseif (isset($info->chains) && $info->chains instanceof Zend_Config) {
				$this->_addChainRoutesFromConfig($name, $route, $info->chains);
			} else {
				$this->addRoute($name, $route);
			}
		}

		return $this;
	}

	/**
	 * Get a route frm a config instance
	 *
	 * @param  Zend_Config $info
	 * @return Zend_Controller_Router_Route_Interface
	 */
	protected function _getRouteFromConfig(Zend_Config $info)
	{
		$class = (isset($info->type)) ? $info->type : 'Zend_Controller_Router_Route';
		if (!class_exists($class)) {
			require_once 'Zend/Loader.php';
			Zend_Loader::loadClass($class);
		}

		$route = call_user_func(array($class, 'getInstance'), $info);

		if (isset($info->abstract) && $info->abstract && method_exists($route, 'isAbstract')) {
			$route->isAbstract(true);
		}

		return $route;
	}

	/**
	 * Add chain routes from a config route
	 *
	 * @param  string                                 $name
	 * @param  Zend_Controller_Router_Route_Interface $route
	 * @param  Zend_Config                            $childRoutesInfo
	 * @return void
	 */
	protected function _addChainRoutesFromConfig($name,
	Zend_Controller_Router_Route_Interface $route,
	Zend_Config $childRoutesInfo)
	{
		foreach ($childRoutesInfo as $childRouteName => $childRouteInfo) {
			if (is_string($childRouteInfo)) {
				$childRouteName = $childRouteInfo;
				$childRoute     = $this->getRoute($childRouteName);
			} else {
				$childRoute = $this->_getRouteFromConfig($childRouteInfo);
			}

			if ($route instanceof Zend_Controller_Router_Route_Chain) {
				$chainRoute = clone $route;
				$chainRoute->chain($childRoute);
			} else {
				$chainRoute = $route->chain($childRoute);
			}

			$chainName = $name . $this->_chainNameSeparator . $childRouteName;

			if (isset($childRouteInfo->chains)) {
				$this->_addChainRoutesFromConfig($chainName, $chainRoute, $childRouteInfo->chains);
			} else {
				$this->addRoute($chainName, $chainRoute);
			}
		}
	}

	/**
	 * Remove a route from the route chain
	 *
	 * @param  string $name Name of the route
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function removeRoute($name)
	{
		if (!isset($this->_routes[$name])) {
			require_once 'Zend/Controller/Router/Exception.php';
			throw new Zend_Controller_Router_Exception("Route $name is not defined");
		}

		unset($this->_routes[$name]);

		return $this;
	}

	/**
	 * Remove all standard default routes
	 *
	 * @param  Zend_Controller_Router_Route_Interface Route
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function removeDefaultRoutes()
	{
		$this->_useDefaultRoutes = false;

		return $this;
	}

	/**
	 * Check if named route exists
	 *
	 * @param  string $name Name of the route
	 * @return boolean
	 */
	public function hasRoute($name)
	{
		return isset($this->_routes[$name]);
	}

	/**
	 * Retrieve a named route
	 *
	 * @param string $name Name of the route
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Router_Route_Interface Route object
	 */
	public function getRoute($name)
	{
		if (!isset($this->_routes[$name])) {
			require_once 'Zend/Controller/Router/Exception.php';
			throw new Zend_Controller_Router_Exception("Route $name is not defined");
		}

		return $this->_routes[$name];
	}

	/**
	 * Retrieve a currently matched route
	 *
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Router_Route_Interface Route object
	 */
	public function getCurrentRoute()
	{
		if (!isset($this->_currentRoute)) {
			require_once 'Zend/Controller/Router/Exception.php';
			throw new Zend_Controller_Router_Exception("Current route is not defined");
		}
		return $this->getRoute($this->_currentRoute);
	}

	/**
	 * Retrieve a name of currently matched route
	 *
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Router_Route_Interface Route object
	 */
	public function getCurrentRouteName()
	{
		if (!isset($this->_currentRoute)) {
			require_once 'Zend/Controller/Router/Exception.php';
			throw new Zend_Controller_Router_Exception("Current route is not defined");
		}
		return $this->_currentRoute;
	}

	/**
	 * Retrieve an array of routes added to the route chain
	 *
	 * @return array All of the defined routes
	 */
	public function getRoutes()
	{
		return $this->_routes;
	}

	/**
	 * Find a matching route to the current PATH_INFO and inject
	 * returning values to the Request object.
	 *
	 * @throws Zend_Controller_Router_Exception
	 * @return Zend_Controller_Request_Abstract Request object
	 */
	public function route(Zend_Controller_Request_Abstract $request)
	{
		if (!$request instanceof Zend_Controller_Request_Http) {
			require_once 'Zend/Controller/Router/Exception.php';
			throw new Zend_Controller_Router_Exception('Zend_Controller_Router_Rewrite requires a Zend_Controller_Request_Http-based request object');
		}

		if ($this->_useDefaultRoutes) {
			$this->addDefaultRoutes();
		}

		// Find the matching route
		foreach (array_reverse($this->_routes) as $name => $route) {
			// TODO: Should be an interface method. Hack for 1.0 BC
			if (method_exists($route, 'isAbstract') && $route->isAbstract()) {
				continue;
			}

			// TODO: Should be an interface method. Hack for 1.0 BC
			if (!method_exists($route, 'getVersion') || $route->getVersion() == 1) {
				$match = $request->getPathInfo();
			} else {
				$match = $request;
			}

			if ($params = $route->match($match)) {
				$this->_setRequestParams($request, $params);
				$this->_currentRoute = $name;
				break;
			}
		}

		return $request;

	}

	protected function _setRequestParams($request, $params)
	{
		foreach ($params as $param => $value) {

			$request->setParam($param, $value);

			if ($param === $request->getModuleKey()) {
				$request->setModuleName($value);
			}
			if ($param === $request->getControllerKey()) {
				$request->setControllerName($value);
			}
			if ($param === $request->getActionKey()) {
				$request->setActionName($value);
			}

		}
	}

	/**
	 * Generates a URL path that can be used in URL creation, redirection, etc.
	 *
	 * @param  array $userParams Options passed by a user used to override parameters
	 * @param  mixed $name The name of a Route to use
	 * @param  bool $reset Whether to reset to the route defaults ignoring URL params
	 * @param  bool $encode Tells to encode URL parts on output
	 * @throws Zend_Controller_Router_Exception
	 * @return string Resulting absolute URL path
	 */
	public function assemble($userParams, $name = null, $reset = false, $encode = true)
	{
		if ($name == null) {
			try {
				$name = $this->getCurrentRouteName();
			} catch (Zend_Controller_Router_Exception $e) {
				$name = 'default';
			}
		}

		$params = array_merge($this->_globalParams, $userParams);

		$route = $this->getRoute($name);
		$url   = $route->assemble($params, $reset, $encode);

		if (!preg_match('|^[a-z]+://|', $url)) {
			$url = rtrim($this->getFrontController()->getBaseUrl(), '/') . '/' . $url;
		}

		return $url;
	}

	/**
	 * Set a global parameter
	 *
	 * @param  string $name
	 * @param  mixed $value
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function setGlobalParam($name, $value)
	{
		$this->_globalParams[$name] = $value;

		return $this;
	}

	/**
	 * Set the separator to use with chain names
	 *
	 * @param  string $separator The separator to use
	 * @return Zend_Controller_Router_Rewrite
	 */
	public function setChainNameSeparator($separator) {
		$this->_chainNameSeparator = $separator;

		return $this;
	}

	/**
	 * Get the separator to use for chain names
	 *
	 * @return string
	 */
	public function getChainNameSeparator() {
		return $this->_chainNameSeparator;
	}
}
